package com.appengine.paranoid_android.lost;

import android.app.KeyguardManager;
import android.app.Service;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.util.Log;

public class InfoService extends Service {
  private static final boolean DEBUG = false;
  private static final String TAG = "Info.Service";
  private static final String NEXT_ALARM = "nextAlarm";

  /**
   * Android internal broadcast intent sent by Alarm Clock to inform the status bar whether an alarm is set or not.
   */
  static final String ACTION_ALARM_CHANGED = "android.intent.action.ALARM_CHANGED";
  /**
   * Donut-only broadcast intent.
   */
  static final String ACTION_POWER_CONNECTED = "android.intent.action.POWER_CONNECTED";
  /**
   * App-specific action: contact details changed.
   */
  static final String ACTION_INFO_MESSAGE_CHANGED = "com.appengine.paranoid_android.lost.INFO_MESSAGE_CHANGED";

  /**
   * Handler message requesting (re)display of the info message.
   */
  private static final int DISPLAY_MESSAGE = 10;
  /**
   * Handler message requesting terminating the service.
   */
  private static final int STOP_SERVICE = 20;

  /**
   * How long after an ALARM_CHANGED broadcast to wait for NEXT_ALARM_FORMATTED changes before stopping the service.
   */
  private static final int ALARM_CHANGING_TIMER = 30000; // 30000;

  private SharedPreferences mPreferences;
  private String mNextAlarm;
  private String mInfoMessage;
  private KeyguardManager mKeyguardManager;

  /**
   * Listens for NEXT_ALARM_FORMATTED changes upon receiving an ALARM_CHANGED broadcast.
   */
  private ContentObserver mContentObserver;
  /**
   * BroadcastReceiver for screen on/off broadcasts.
   */
  private InfoBroadcastReceiver mScreenOnOffReceiver;

  /**
   * Whether the lock screen currently displays the contact info or the device is not locked and the contact info will
   * definitely be displayed when it locks. When the service is starting up we don't know this, so it's initially false.
   */
  private boolean mMessageDisplayed = false;

  /**
   * Whether the service has been started by an alarm changed message.
   *
   * If this is true, the service keeps running for a while before terminating, listening for any following alarm
   * changed or screen on/off messages. Otherwise the service terminates immediately after the message is set.
   */
  private boolean mAlarmChanging = false;

  private final Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      if (DEBUG) Log.d(TAG, "Handler: handleMessage(" +
                       (msg.what == DISPLAY_MESSAGE ? "DISPLAY_MESSAGE" : "STOP_SERVICE") + ")");
      setInfoMessage();

      if (msg.what == DISPLAY_MESSAGE) {
        if (!mAlarmChanging) {
          if (DEBUG) Log.d(TAG, "Message set. Exiting.");
          stopSelf();
        }
      } else if (msg.what == STOP_SERVICE) {
        // It looks like every time when the timer expires and the message is not displayed it's because the alarm is
        // still ringing, so we don't have to worry about the lock screen: it will display the correct info once the
        // alarm terminates one way or another.
        if (DEBUG) Log.d(TAG, "Alarm changing timer expired. Exiting.");
        stopSelf();
      }
    }
  };

  @Override
  public void onCreate() {
    if (DEBUG) Log.d(TAG, "onCreate()");

    mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
    mInfoMessage = mPreferences.getString(InfoSetup.MESSAGE_KEY, null);
    mNextAlarm = mPreferences.getString(NEXT_ALARM, null);
    mKeyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
  }

  @Override
  public void onStart(Intent intent, int startId) {
    String action = intent.getAction();
    if (DEBUG) Log.d(TAG, "onStart(" + action + ")");
    if (mInfoMessage == null) {
      if (DEBUG) Log.d(TAG, "No message to set. Exiting.");
      stopSelf();
      return;
    }

    if (Intent.ACTION_SCREEN_OFF.equals(action)) {
      // Screen is turning off: force display message.
      if (!mMessageDisplayed) {
        forceDisplayMessage();
      }
      return;
    } else if (ACTION_ALARM_CHANGED.equals(action) || Intent.ACTION_BOOT_COMPLETED.equals(action)) {
      // Alarm changed: set message; if the message wasn't changed yet retry a few times. Alarm Clock actually fires
      // the broadcast before it sets the next alarm message so delay setting the message a bit.
      // TODO Use the alarmSet extra in the intent to double-check whether the change actually happened.
      mAlarmChanging = true;
//      if (DEBUG) Log.d(TAG, "Unnecessary reset.");
//      mMessageDisplayed = false;

      // Register a ContentObserver to listen for system settings changes and an IntentFilter for screen on/off
      // broadcasts.
      if (mContentObserver == null) {
        mContentObserver = new ContentObserver(mHandler) {
          @Override
          public void onChange(boolean selfChange) {
            if (DEBUG) Log.d(TAG, " === System settings changed. === ");
            setInfoMessage();
          }
        };
        getContentResolver().registerContentObserver(Settings.System.CONTENT_URI, true, mContentObserver);

        // Unfortunately SCREEN_OFF intents are only broadcasted to programmatically registered receivers (i.e. not to
        // receivers listed in the manifest), so register one when the alarm is changing.
        IntentFilter filter = new IntentFilter();
        filter.addAction(Intent.ACTION_SCREEN_ON);
        filter.addAction(Intent.ACTION_SCREEN_OFF);
        mScreenOnOffReceiver = new InfoBroadcastReceiver();
        registerReceiver(mScreenOnOffReceiver, filter);
      }

      // Remove all other display messages, including other "alarm changing" ones.
      enqueueHandlerMessage(DISPLAY_MESSAGE, 0);
      // Set the "stop service" message timer.
      enqueueHandlerMessage(STOP_SERVICE, ALARM_CHANGING_TIMER);
      return;
    } else if (ACTION_INFO_MESSAGE_CHANGED.equals(action)) {
      // Info message changed by InfoSetup. No need to worry about force displaying the message, screen is not locked.
      mInfoMessage = mPreferences.getString(InfoSetup.MESSAGE_KEY, null);
    }
    if (!mAlarmChanging && isMessageSet()) {
      // Random broadcast, message is already set: terminate.
      if (DEBUG) Log.d(TAG, "Alarm not changed. Exiting.");
      stopSelf();
      return;
    }
    enqueueHandlerMessage(DISPLAY_MESSAGE, 0);
  }

  @Override
  public void onDestroy() {
    if (DEBUG) Log.d(TAG, "onDestroy()");
    if (mContentObserver != null) {
      getContentResolver().unregisterContentObserver(mContentObserver);
      unregisterReceiver(mScreenOnOffReceiver);
    }
  }

  /**
   * Set the info message as the next system alarm text.
   *
   * @param screenLocking  Whether the screen is being locked. If that is the case, force the lock screen to update by
   *                       disabling, then reenabling it.
   * @return  <code>true</code> if the message was changed, false if it needed no update (meaning a retry might be
   *          necessary).
   */
  private void setInfoMessage() {
    if (DEBUG) Log.d(TAG, "setInfoMessage()");

    String message = mInfoMessage;
    if (message == null) {
      if (DEBUG) Log.d(TAG, "No message to set/display. Exiting.");
      mMessageDisplayed = true;
      mAlarmChanging = false;
      return;
    }

    if (isMessageSet()) {
      updateMessageDisplayedFlag();
      return;
    }

    mMessageDisplayed = false;
    String displayedAlarm = getDisplayedMessage();
    if (displayedAlarm == null || displayedAlarm.indexOf('\n') == -1) {
      // Message was changed from the outside and it's empty or only one line of text: this is the next alarm, so
      // remember it.
      if (DEBUG) Log.d(TAG, "Next alarm: " + displayedAlarm);
      if ((mNextAlarm == null && displayedAlarm != null) || mNextAlarm != null && !mNextAlarm.equals(displayedAlarm)) {
        mNextAlarm = displayedAlarm;
        SharedPreferences.Editor edit = mPreferences.edit();
        edit.putString(NEXT_ALARM, displayedAlarm);
        edit.commit();
      }
    } else {
      if (DEBUG) Log.d(TAG, "Currently displayed message: " + displayedAlarm);
    }

    if (mNextAlarm != null && mNextAlarm.length() > 0) {
      // Prepend next alarm to message.
      message = String.format("%s\n\n%s", mNextAlarm, message);
    }
    if (DEBUG) Log.d(TAG, "Set next alarm to: " + message);

    Settings.System.putString(getContentResolver(),
                              Settings.System.NEXT_ALARM_FORMATTED,
                              message);
    updateMessageDisplayedFlag();
  }

  private void forceDisplayMessage() {
    // Seems to be the only way of forcing the lock screen to update: disable and reenable the keyguard.
    // Furthermore, disabling then quickly reenabling the lockscreen seems to cause some sort of deadlock, so
    // this has to be done carefully.
    if (DEBUG) Log.d(TAG, "Screen is off and locked. Forcing display.");
    Intent keyguard_hack = new Intent(InfoService.this, LockScreenUpdater.class);
    keyguard_hack.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_FROM_BACKGROUND);
    startActivity(keyguard_hack);
    mMessageDisplayed = true;
  }

  private boolean isScreenLocked() {
    if (mKeyguardManager.inKeyguardRestrictedInputMode()) {
      if (DEBUG) Log.d(TAG, "Screen is locked.");
      return true;
    } else {
      if (DEBUG) Log.d(TAG, "Screen is unlocked: message will be displayed on next lock screen.");
      return false;
    }
  }

  private String getDisplayedMessage() {
    return Settings.System.getString(getContentResolver(), Settings.System.NEXT_ALARM_FORMATTED);
  }

  private boolean isMessageSet() {
    String message = getDisplayedMessage();
    if (message != null && message.endsWith(mInfoMessage)) {
      if (DEBUG) Log.d(TAG, "Message already set.");
      return true;
    } else {
      if (DEBUG) Log.d(TAG, "Message has been changed from outside.");
      mMessageDisplayed = false;
      return false;
    }
  }

  private void updateMessageDisplayedFlag() {
    if (mMessageDisplayed) {
      if (DEBUG) Log.d(TAG, "Message already displayed.");
    } else if (!mMessageDisplayed && !isScreenLocked()) {
      mMessageDisplayed = true;
    }
  }

  /**
   * Equeue message <code>what</code> with <code>delayMillis</code> delay if such a message isn't already enqueued.
   * If an identical message already exists:
   * <ul>
   *   <li>and <code>delayMillis == 0</code>: nothing happens;
   *   <li>and <code>delayMillis &gt; 0</code>: the message is replaced with one with a <code>
   */
  private void enqueueHandlerMessage(int what, int delayMillis) {
    if (delayMillis != 0) {
      mHandler.removeMessages(what);
    } else if (mHandler.hasMessages(what)) {
      return;
    }

    mHandler.sendMessageDelayed(mHandler.obtainMessage(what), delayMillis);
  }

  @Override
  public IBinder onBind(Intent intent) {
    // No interface to bind to.
    return null;
  }
}
